/************************************************************
 * Copyright (c) 2010 Georg Fritzsche
 *
 * This project is licensed under the "New BSD License": 
 * http://www.opensource.org/licenses/bsd-license.php
 ***********************************************************/

#include "midi_listener.h"

#include <vector>
#include <map>
#include <sstream>
#include <iomanip>
#include <boost/assign/list_of.hpp>
#include <boost/bind.hpp>

#include "RtError.h"
#include "RtMidi.h"

#include "player.h"
#include "config.h"
#include "util.h"


namespace
{   
    typedef MidiListener::Message    Message;
    typedef MidiListener::Command    Command;
    typedef MidiListener::CommandMap CommandMap;
    
    bool message_lesser(const Message& a, const Message& b)
    {
        return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
    }
    
    CommandMap init_command_map(const Config& config) 
    {
        const Message MessageNext  = config.midiActionMap().find(MidiListener::ActionNext )->second;
        const Message MessageReset = config.midiActionMap().find(MidiListener::ActionReset)->second;

        CommandMap m(&message_lesser);
        m[MessageNext ] = boost::bind(&Player::next , _1);
        m[MessageReset] = boost::bind(&Player::reset, _1);
        
        return CommandMap(m.begin(), m.end(), &message_lesser);
    }

    std::string to_hex_string(unsigned char c) 
    {
        std::ostringstream is;
        is << std::setw(2) << std::setfill('0') << std::hex << (unsigned)c;
        return is.str();
    }
    
    std::string message_to_string(const MidiListener::Message& message)
    {
        std::ostringstream os;
        os << "[";
        for (Message::size_type i=0, count=message.size(); i<count; ++i) {
            os << to_hex_string(message[i]);
            if (i < (count-1)) os << ", ";
        }
        os << "]";
        return os.str();
    }
}

MidiListener::MidiListener(const boost::shared_ptr<Player>& player, const Config& config)
try
  : m_player(player)
  , m_commandMap(init_command_map(config))
{
    m_midiIn.reset(new RtMidiIn());
        
    // Check available ports vs. specified.

    const unsigned portCount = m_midiIn->getPortCount();
    dtrace("MIDI port-count: " << portCount);
    
    if ((config.midiPort() >= portCount) || (config.midiPort() == invalid_port)) {
        throw midi_error("invalid port specifier");
    }
    
    m_midiIn->openPort(config.midiPort());
    
    // Set our callback function.  This should be done immediately after
    // opening the port to avoid having incoming messages written to the
    // queue instead of sent to the callback function.
    
    m_midiIn->setCallback(&midi_callback, this);

    // Set filters for sysex, timing, and active sensing messages.
    
    m_midiIn->ignoreTypes(true, true, true);
}
catch (RtError& e) 
{
    throw midi_error(e.getMessage());
}


MidiListener::~MidiListener()
{
    if (m_midiIn.get()) {
        m_midiIn->cancelCallback();
    }
}

void MidiListener::midi_callback(double deltatime, Message* message_ptr, void* userData)
{
    if (!message_ptr) return;
    if (!userData)    return;

    const Message& message = *message_ptr;
    dtrace("MIDI message: " << message_to_string(message));
    
    static_cast<MidiListener*>(userData)->execute(message);
}

void MidiListener::execute(const Message& message)
{
    CommandMap::const_iterator it = m_commandMap.find(message);
    if (it != m_commandMap.end()) {
        dtrace("recognized command");
        it->second(m_player.get());
    }
}

void MidiListener::listPorts()
{
    boost::shared_ptr<RtMidiIn> midiIn;
    
    try {
        midiIn.reset(new RtMidiIn());
    } catch (RtError& e) {
        throw midi_error(e.getMessage());
    }
    
    const unsigned portCount = midiIn->getPortCount();
    
    trace("MIDI ports:");
    
    for (unsigned port = 0; port < portCount; ++port)
    {
        try {
            trace(" * port #" << port << ": " << midiIn->getPortName(port));
        } catch (RtError&) {
            
        }
    }
}
